本日重點:實作: toByte, ADRS member function
SLH-DSA 的簽章需要用到金鑰,金鑰的產生需要用到公私鑰種子,這些 seed 需要透過 approved random bit generator (RBG) 來獲得,我將用3天的篇幅來說明如何 產生 SLH-DSA 金鑰,以及如何實現跨平台。
approved 在 FIPS 205 具有很重要的意義, 他代表 FIPS-approved and/or NIST-recommended, 如果直接用中文, 恐怕難以表達他的意義, 所以本文直接用 approved
在我們開始實現 SLH-DSA 金鑰產生 之前, 有必要先澄清一件事 ---- 能不能直接用 Secure Element (SE) 產生 SLH-DSA 金鑰?
不行,至少截至 2025-09-14 還不行。
SE 普遍支援 產生RSA金鑰,但截至 2025-09-14,市售的 SE 還不支援 產生SLH-DSA 金鑰。所以,還不能 直接從 SE 獲得 SLH-DSA 金鑰,我們只能在 SE 外部去產生 SLH-DSA 金鑰。所謂的外部,可以是 MCU (例如 M33 或 M4),也可以是 FPGA,也可以是 SoC,甚至是自己設計的 PCB。一旦未來 SE 支援產生SLH-DSA 金鑰,就能直接用 SE 產生 SLH-DSA 金鑰。
雖然 SE 不能直接產生 SLH-DSA key pair,但 SE 仍然有它的作用,幾天後我將對此做更進一步的探討,今天不會對 SE 有過多的著墨。
FIPS 205 第 34 頁的 Algorithm 18 slh_keygen_internal 是產生 SLH-DSA key pair 的演算法,如下所示
Algorithm 18 slh_keygen_internal(SK.seed, SK.prf, PK.seed)
Generates an SLH-DSA key pair.
Input: Secret seed SK.seed, PRF key SK.prf, public seed PK.seed Output: SLH-DSA key pair (SK, PK).
1: ADRS ← toByte(0, 32) ▷ generate the public key for the top-level XMSS tree
2: ADRS.setLayerAddress(𝑑 −1)
3: PK.root ← xmss_node(SK.seed, 0, ℎ′,PK.seed,ADRS)
4: return ((SK.seed, SK.prf, PK.seed, PK.root), (PK.seed, PK.root) )
Day 2、Day 3、Day 4 將基於此演算法,參考 SPHINCS+ reference code,然後寫 toByte
、xmss_node
和 ADRS 的 member function setLayerAddress
。
基於跨平台的考量,random bit generator (RBG) 需要一個抽象層,好處是如果想更換底層的亂數產生器或是更換平台,可以不用修改 slh_keygen_internal,只需要提供不同的 HAL, 就能在不改程式的情況下直接更換亂數產生器,例如從軟體版換成硬體版(Arm 的 CryptoCell-310 或 CryptoCell-312, 或是其他),而且還有一個好處,就是在 CryptoCell 導入前就能用 軟體版的亂數產生器 先完成 SLH-DSA key pair 的產生,然後等到整個簽章都完成了,再改用 CryptoCell-310 或 CryptoCell-312 來重新一次這個流程 "產生亂數 -> 產生 key pair",這些會在 Day 4 詳述。
slh_keygen_internal
用來產生 key pair,它的演算法在 FIPS 205 的第 34 頁,這裡要寫的有 toByte
、xmss_node
以及 setLayerAddress
,以下 breakdown slh_keygen_internal
還有哪些 function 要寫
xmss_node
的演算法在 FIPS 205 的第 23 頁,這裡要寫的有 wots_pkGen、setTypeAndClear、setKeyPairAddress、setTreeHeight、setTreeIndex、H
wots_pkGen
的演算法在 FIPS 205 的第 18 頁,這裡要寫的還有 function 有 setTypeAndClear、setKeyPairAddress、getKeyPairAddress、setChainAddress、PRF、chain、T
chain
的演算法在 FIPS 205 的第 18 頁,這裡要寫的 function 有 setHashAddress、F
H、T、F 這些 function name 只有一個字,Day 3、Day4 會陸續說明
至此,盤點一下有什麼 function 要寫:
上述的 2 到 9 是 ADRS 的 member function,由於 C11 沒有 member function 的機制,如果在 struct 裡放 function pointer 模擬 call 物件的 method, 也是一種方法,不過這樣仍然要放一個類似 this 的 pointer 做為參數,還不如直接用 helper function + pointer,所以,上述的 2 到 9 會用 helper function 的方式去實現。
ADRS 在上述 16 個 function 都會用到,FIPS 205 的第 12 頁是 ADRS 的格式,它在記憶體佔了 32 bytes,所以我們用 unsigned char[32] 來表示 ADRS。
這是 ADRS 的結構, page 12, FIPS 205
我們己經將 ADRS 定義為 unsigned char[32],layer address 佔了 4 個 byte,tree address 佔了 12 個 bytes,要對他們賦值,直接用 memcpy 就可以了,不過,layer address、tree address、type 都是一個數字,要完成 n-bytes 的賦值,需要先將數字轉成 byte array,然後才能用 memcpy,將數字轉成 byte array,就要用到 toByte。
舉例來說,如果要指定 layer address,就像這樣
unsigned char S[4];
toByte((unsigned long long)layer, 4, S);
memcpy(adrs, S, 4); // 0, 1, 2, 3
Figure 2 沒有看到 key pair address、chain address 和 hash address,因為 ADRS 的 type 有 7 種: WOTS_HASH、WOTS_PK,
TREE、FORS_TREE、FORS_ROOTS、WOTS_PRF 和 FORS_PRF,每一種 type,前 20 個 bytes 都一樣,由 layer address、tree address 和 tree 所組成,剩下的 12 個 bytes 不完全相同,相關的 member function 將會在本系列的文章中陸續提及。
這是 WOTS+ hash address 的結構 (type = WOTS_HASH),page 12, FIPS 205
key pair address 未必在每一個 type 都有,但如果有,他都是從 index 20 開始,所以如果要指定 key pair address,就像這樣
unsigned char key_pair_addr[4];
toByte(i, 4, key_pair_addr);
// ADRS[20, 21, 22, 23]
memcpy(adrs + 20, key_pair_addr, 4); // 20, 21, 22, 23
指定 chain address 和 hash address 依此類推。
至此,我們完成了 setLayerAddress、setKeyPairAddress、setChainAddress 和 setHashAddress。這些,都建立在 toByte 能將一個數字轉成 byte array 的基礎上,現在我們來看 toByte 是怎麼轉的。以下是 FIPS 205 第15頁,toByte 的演算法
注意,這是 big-endian。
big-endian 跟 little-endian 在記憶體裡放 array element 是不一樣的順序,舉例來說,同樣都是 1,它在 4 byte array 的 index,會有以下的差異。
這是 little-endian
a[0] = 0x01
a[1] = 0x00
a[2] = 0x00
a[3] = 0x00
這是 big-endian
a[0] = 0x00
a[1] = 0x00
a[2] = 0x00
a[3] = 0x01
回到寫 toByte,total & 0xff
就是取最低的那一個 byte,取完就 assign 給 byte array,接著 total = total >> 8
,直到 byte array 每一個 element 都被賦值。
for (unsigned int i = 0; i < n; ++i) {
pS[n - 1 - i] = (unsigned char)total & 0xff;
// total ← total >> 8 ▷ least significant 8 bits of 𝑡𝑜𝑡𝑎𝑙
total = total >> 8;
}
這個賦值的過程,是從 array[n-1] 開始直到 array[0],因為這是 big-endian
至此,我們完成了 toByte 和 4 個 ADRS 的 member function,Day 3、Day 4 會陸續完成 產生 SLH-DSA 金鑰 所需要的 function。